/*
 * Zed Attack Proxy (ZAP) and its related class files.
 *
 * ZAP is an HTTP/HTTPS proxy for assessing web application security.
 *
 * Copyright 2011 The ZAP Development Team
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.zaproxy.zap.extension.httppanel.view.hex;

import java.util.ArrayList;
import java.util.List;
import javax.swing.table.AbstractTableModel;
import org.zaproxy.zap.utils.ByteBuilder;

public class HttpPanelHexModel extends AbstractTableModel {

    private static final String[] hexSymbols = {
        "0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "a", "b", "c", "d", "e", "f"
    };
    private static final int BITS_PER_HEX_DIGIT = 4;
    private static final int COLUMN_COUNT = 34;

    private static final long serialVersionUID = 1L;
    private List<String[]> listRow = new ArrayList<>();
    private boolean editable = false;
    private boolean changed = false;

    public boolean isEditable() {
        return editable;
    }

    public void setEditable(boolean editable) {
        this.editable = editable;
    }

    public HttpPanelHexModel() {
        super();
    }

    @Override
    public int getColumnCount() {
        return COLUMN_COUNT;
    }

    @Override
    public int getRowCount() {
        return listRow.size();
    }

    @Override
    public Object getValueAt(int row, int col) {
        return listRow.get(row)[col];
    }

    private boolean isHexColumn(int col) {
        return col > 0 && col <= 16;
    }

    private boolean isCharColumn(int col) {
        return col > 17;
    }

    @Override
    public String getColumnName(int col) {
        if (isHexColumn(col)) {
            return Integer.toHexString(col - 1);
        } else if (isCharColumn(col)) {
            return Integer.toHexString(col - 18);
        }
        return "";
    }

    public static String toHexFromByte(final byte b) {
        byte leftSymbol = (byte) ((b >>> BITS_PER_HEX_DIGIT) & 0x0f);
        byte rightSymbol = (byte) (b & 0x0f);

        return (hexSymbols[leftSymbol] + hexSymbols[rightSymbol]);
    }

    private String[] newRow() {
        String[] row = new String[COLUMN_COUNT];
        row[0] = String.format("%08x", (listRow.size() * 16));
        return row;
    }

    private void setHex(String[] row, int index, byte b) {
        row[index] = toHexFromByte(b);
        row[index + 17] = String.valueOf((char) b);
    }

    private void setChar(String[] row, int index, char c) {
        row[index - 17] = toHexFromByte((byte) c);
        row[index] = String.valueOf(c);
    }

    public synchronized void setData(byte[] body) {
        listRow.clear();

        if (body.length != 0 || isEditable()) {
            int index = 0;
            int rowIndex = 0;
            String[] row = newRow();
            listRow.add(row);
            while (index < body.length) {
                setHex(row, rowIndex + 1, body[index]);
                rowIndex++;
                if (rowIndex >= 16) {
                    row = newRow();
                    listRow.add(row);
                    rowIndex = 0;
                }
                index++;
            }
        }

        this.fireTableDataChanged();
        changed = false;
    }

    public synchronized byte[] getData() {
        // Need to implement if/when edit supported
        ByteBuilder bb = new ByteBuilder();
        for (String[] row : listRow) {
            for (int i = 1; i < 17; i++) {
                if (row[i] == null || row[i].length() == 0) {
                    break;
                }
                bb.append((char) Integer.parseInt(row[i], 16));
            }
        }
        return bb.toByteArray();
    }

    @Override
    public boolean isCellEditable(int rowIndex, int columnIndex) {
        if (!isEditable()) {
            return false;
        }
        if (!(isHexColumn(columnIndex) || isCharColumn(columnIndex))) {
            // Either the index or spacer column
            return false;
        }
        if (rowIndex < listRow.size() - 1) {
            // Not the last row, so no problem
            return true;
        }

        String[] row = listRow.get(rowIndex);
        if (!isCharColumn(columnIndex)) {
            // Previous cell is null - can only edit the first null cell
            return (row[columnIndex - 1] != null);
        } else if (row[columnIndex - 1] == null && columnIndex != 18) {
            // Previous cell is null and it's not the first text cell
            return false;
        }

        return true;
    }

    @Override
    public void setValueAt(Object value, int row, int col) {
        String val = (String) value;
        byte byteVal = -1;
        char charVal = (char) 0;
        if (isHexColumn(col)) {
            if (val.length() > 2) {
                return;
            }
            try {
                byteVal = (byte) Integer.parseInt(val, 16);
            } catch (NumberFormatException e) {
                return;
            }
        } else if (isCharColumn(col)) {
            if (val.length() != 1) {
                return;
            }
            charVal = val.charAt(0);
        } else {
            return;
        }
        String[] cell = listRow.get(row);
        if (isHexColumn(col)) {
            setHex(cell, col, byteVal);
        } else {
            setChar(cell, col, charVal);
        }

        int lastRowChanged = row;

        if ((row == listRow.size() - 1) && cell[16] != null) {
            // Last cell on last line is used, add a new row
            listRow.add(newRow());
            lastRowChanged++;
        }

        fireTableRowsUpdated(row, lastRowChanged);
        changed = true;
    }

    public boolean hasChanged() {
        return changed;
    }
}
